#include "cppcodec/base32_rfc4648.hpp"
#include "resolver.h"
#include "funcs.h"
#include "HTTPheaders.h"

#include <QTcpSocket>
#include <QRegularExpression>
#include <QJsonArray>
#include <QDateTime>
#include <QHostInfo>
#include <QDebug>
#include <QFile>
#include <array>

#ifdef _WIN32
    #include <ws2tcpip.h>
#else
    #include <arpa/inet.h>
#endif

constexpr int LIMIT_TO_READ = 2048;

Resolver::Resolver(const QString& addr, quint16 port, QObject* parent) :
    QObject(parent),
    m_tcpServer(new QTcpServer),
    m_address(addr),
    m_port(port),
    m_NoApi(false),
    m_NoHttp(false),
    m_NoResolve(false)
{
    if (!m_tcpServer->listen(m_address, m_port)) {
        throw "Server not binded";
    }

    qInfo().noquote() << "<-" << m_tcpServer->serverAddress().toString() + " : " +
           QString::number(m_tcpServer->serverPort()) << "[" + QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz") + "]";

    connect(m_tcpServer, SIGNAL(newConnection()), this, SLOT(slotNewUser()));
}

void Resolver::disableAPI()
{
    m_NoApi = true;
}

void Resolver::disableWebPage()
{
    m_NoHttp = true;
}

void Resolver::disableResolv()
{
    m_NoResolve = true;
    http::HTML_PAGE.replace("IPv6 address or domain", "IPv6 address or meship domain");
}

void Resolver::slotNewUser()
{
    if (!m_tcpServer->isListening()) return;

    QTcpSocket* clientSocket = m_tcpServer->nextPendingConnection();
    connect (clientSocket, SIGNAL(readyRead()), this, SLOT(slotReadyClient()));
    connect (clientSocket, SIGNAL(disconnected()), clientSocket, SLOT(deleteLater()));
}

void Resolver::slotReadyClient()
{
    QTcpSocket* clientSocket = static_cast<QTcpSocket*>(sender());
    QString req = clientSocket->read(LIMIT_TO_READ);
    qInfo().noquote() << "\n->" << clientSocket->peerAddress().toString()
                       << "[" + QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz") + "]";

    QSharedPointer<QTextStream> ts(new QTextStream(clientSocket));

    if (m_NoHttp and (req.startsWith("HEAD /") or req.startsWith("GET /toConverting") or req.startsWith("GET / ")))
    {
        qInfo().noquote() << "   └ HTML (dropped)";
        *ts << http::HEADER_REJECT;
    }
    else if (m_NoApi and req.startsWith("GET /api/"))
    {
        qInfo().noquote() << "   └ API (dropped)";
        JsonAnswer a;
        a.setValue("status", false);
        *ts << a.result();
    }

    else if (req.startsWith("GET /api/"))
    {
        qInfo().noquote() << "   └ API";
        req.contains("toConverting=") ?
            processPage(req, ts, false) :
            startPage(ts, false);
    }
    else if (req.startsWith("GET /"))
    {
        qInfo().noquote() << "   └ HTML";
        req.contains("toConverting=") ?
            processPage(req, ts, true) :
            startPage(ts, true);
    }

    else if (req.startsWith("HEAD /"))
    {
        qInfo().noquote() << "   └ HEAD request";
        *ts << http::HEADER_OK;
    }

    clientSocket->close();
}

void Resolver::convertStrToRaw(const QString &str, Address &array)
{
    inet_pton(AF_INET6, str.toUtf8(), (void*)array.data());
}

QString Resolver::getBase32(const Address &rawAddr)
{
    return cppcodec::base32_rfc4648::encode(rawAddr.data(), ADDRIPV6_SIZE).c_str();
}

QString Resolver::decodeMeshToIP(const QString &meshname)
{
    std::string mesh = pickupStringForMeshname(meshname.toStdString()) + "======"; // 6 паддингов - норма для IPv6 адреса
    std::vector<uint8_t> raw;
    try {
        raw = cppcodec::base32_rfc4648::decode(mesh);
    } catch (cppcodec::padding_error const&) {
        return QString();
    } catch (cppcodec::symbol_error const&) {
        return QString();
    }

    Address rawAddr;
    for(int i = 0; i < 16; ++i)
        rawAddr[i] = raw[i];
    return getAddress(rawAddr);
}

std::string Resolver::pickupStringForMeshname(std::string str)
{
    bool dot = false;
    std::string::iterator delend;
    for (auto it = str.begin(); it != str.end(); it++)
    {
        *it = toupper(*it); // делаем все буквы заглавными для обработки
        if(*it == '.') {
            delend = it;
            dot = true;
        }
    }
    if (dot)
        for (auto it = str.end(); it != delend; it--)
            str.pop_back(); // удаляем доменную зону
    return str;
}

QString Resolver::getAddress(const Address& rawAddr)
{
    char ipStrBuf[46];
    inet_ntop(AF_INET6, rawAddr.data(), ipStrBuf, 46);
    return QString(ipStrBuf);
}


void Resolver::startPage(QSharedPointer<QTextStream> ts, bool html)
{
    if (html)
    {
        qInfo().noquote() << "     └ Start page";
        QString page = http::HTML_PAGE;
        QString css = http::CSS_DEFAULT;

        QString text;
        if (m_NoApi and not m_NoResolve)
        {
            text = "<p>Input any domain to resolve or IPv6 to convert to <a href=\"https://github.com/zhoreeq/meshname\" style=\"text-decoration: none\">meship</a>.</p>";
        }
        else if (m_NoApi and m_NoResolve)
        {
            text = "<p>Input meship domain to resolve or IPv6 to convert to <a href=\"https://github.com/zhoreeq/meshname\" style=\"text-decoration: none\">meship</a>.</p>";
        }
        else if (not m_NoResolve)
        {
            text = "<p>Input any domain to resolve or IPv6 to convert to <a href=\"https://github.com/zhoreeq/meshname\" style=\"text-decoration: none\">meship</a>.</p>"
                   "<p>Also you can use Mario DNS tool via API to get result in JSON (GET request like a /api/toConverting=yourvalue).";
        }
        else
        { // no resolve
            text = "<p>Input meship domain to resolve or IPv6 to convert to <a href=\"https://github.com/zhoreeq/meshname\" style=\"text-decoration: none\">meship</a>.</p>"
                   "<p>Also you can use Mario DNS tool via API to get result in JSON (GET request like a /api/toConverting=yourvalue).";
        }

        css.replace("{{COLOR}}", "gray");
        page.replace("{{STYLES}}", css);
        page.replace("{{TEXT}}", text);
        *ts << page;
    }
    else
    {
        QString msg;
        if (m_NoResolve) {
            msg = "Push meship domain to resolve or IPv6 to convert to meship like a /api/toConverting=yourvalue";
        } else {
            msg = "Push any domain to resolve or IPv6 to convert to meship like a /api/toConverting=yourvalue";
        }
        qInfo().noquote() << "     └ Incorrect request";
        JsonAnswer a;
        a.setValue("status", false);
        a.setValue("answer", msg);
        *ts << a.result();
    }
}

void Resolver::startPageWithMessage(const QString & value, QSharedPointer<QTextStream> ts, const QString & msg, bool html)
{
    qInfo().noquote() << "       └ " + msg;

    if (html)
    {
        QString page = http::HTML_PAGE;
        QString css = http::CSS_DEFAULT;
        css.replace("{{COLOR}}", "red");
        page.replace("{{STYLES}}", css);
        page.replace("{{TEXT}}", msg);
        if (!value.isEmpty()) {
            page.replace(QRegularExpression("placeholder=\".*domain\""), "value=\"" + value + "\"");
        }
        *ts << page;
    }
    else
    {
        JsonAnswer a;
        a.setValue("status", false);
        a.setValue("answer", msg);
        *ts << a.result();
    }
}

void Resolver::processPage(const QString& req, QSharedPointer<QTextStream> ts, bool html)
{
    QString value = funcs::getValue(req, "toConverting");
    value.remove('+');
    value = QByteArray::fromPercentEncoding(value.toUtf8());

    qInfo().noquote() << "     └ " + value;
    const uint8_t MAX_LEN = 60;
    if (value.size() > MAX_LEN)
    {
        startPageWithMessage("", ts, "Maximum input value length = " + QString::number(MAX_LEN), html);
    }

    else if (value == "version") {
        toVersion(value, ts, html);
    }

    else if (value.contains(":")) {
        toMeship(value, ts, html);
    }

    else if (value.endsWith(".meship")) {
        toIp(value, ts, html);
    }

    else if (value.contains(".") and not m_NoResolve) {
        toRealResolv(value, ts, html);
    }

    else
    {
        QString text;
        m_NoResolve
          ? text ="Input value must contains meship domain or IPv6"
          : text = "Input value must contains domain or IPv6";
        startPageWithMessage(value, ts, text, html);
    }
}

void Resolver::toMeship(const QString & value, QSharedPointer<QTextStream> ts, bool html)
{
    Address rawAddr;
    convertStrToRaw(value, rawAddr);
    QString base32 = getBase32(rawAddr);
    base32 = base32.toLower();
    base32.remove('=');
    base32 += ".meship";

    if (html)
    {
        QString page = http::HTML_PAGE;
        QString css = http::CSS_DEFAULT;
        css.replace("{{COLOR}}", "green");
        page.replace("{{STYLES}}", css);
        page.replace("{{TEXT}}", base32);
        page.replace(QRegularExpression("placeholder=\".*domain\""), "value=\"" + value + "\"");
        *ts << page;
    }
    else
    {
        JsonAnswer a;
        a.setValue("status", true);
        a.setValue("answer", base32);
        *ts << a.result();
    }
}

void Resolver::toIp(const QString & value, QSharedPointer<QTextStream> ts, bool html)
{
    QString result = decodeMeshToIP(value);
    if (result.isEmpty()) {
        startPageWithMessage(value, ts, "Failed: meship domain must have 26 base32 (RFC4648) characters only", html);
        return;
    }

    if (html)
    {
        QString page = http::HTML_PAGE;
        QString css = http::CSS_DEFAULT;
        css.replace("{{COLOR}}", "green");
        page.replace("{{STYLES}}", css);
        page.replace("{{TEXT}}", result);
        page.replace(QRegularExpression("placeholder=\".*domain\""), "value=\"" + value + "\"");
        *ts << page;
    }
    else
    {
        JsonAnswer a;
        a.setValue("status", true);
        a.setValue("answer", result);
        *ts << a.result();
    }
}

void Resolver::toRealResolv(const QString & value, QSharedPointer<QTextStream> ts, bool html)
{
    QHostInfo host = QHostInfo::fromName(value);
    if (host.error() == QHostInfo::NoError)
    {
        auto addrs = host.addresses();

        if (html)
        {
            QString addresses;
            for (auto a: addrs) {
                addresses += a.toString() + "<br>";
            }
            QString page = http::HTML_PAGE;
            QString css = http::CSS_DEFAULT;
            css.replace("{{COLOR}}", "green");
            page.replace("{{STYLES}}", css);
            page.replace("{{TEXT}}", addresses);
            page.replace(QRegularExpression("placeholder=\".*domain\""), "value=\"" + value + "\"");
            *ts << page;
        }
        else
        {
            QJsonArray array;
            for (const auto& address: addrs)
            {
                array.push_back(address.toString());
            }
            JsonAnswer a;
            a.setValue("status", true);
            a.setValue("answer", array);
            *ts << a.result();
        }
    }
    else
    {
        QString msg;
        if (value.endsWith(".meshname")) {
            msg = "Domain not resolved. Try \".meship\" instead \".meshname\" for direct translation to address";
        } else {
            msg = "Failed: " + host.errorString();
        }
        startPageWithMessage(value, ts, msg, html);
    }
}

void Resolver::toVersion(const QString &value , QSharedPointer<QTextStream> ts, bool html)
{
    if (html)
    {
        QString page = http::HTML_PAGE;
        QString css = http::CSS_DEFAULT;
        css.replace("{{COLOR}}", "gray");
        page.replace("{{STYLES}}", css);
        page.replace("{{TEXT}}", VERSION);
        page.replace(QRegularExpression("placeholder=\".*domain\""), "value=\"" + value + "\"");
        *ts << page;
    }
    else
    {
        JsonAnswer a;
        a.setValue("status", true);
        a.setValue("answer", VERSION);
        *ts << a.result();
    }
}
